﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Utility.Inspector;
using Utility.Math;
using System;

namespace Utility.NodeGraphs
{
	public class NodeGraphEditorWindow : EditorWindow
	{
		public NodeGraph NodeGraph { get; protected set; }
		public Vector2 MousePosition { get; private set; }
		public Vector2 MousePosition_Canvas { get; private set; }

		#region editor
		static Dictionary<NodeGraph, NodeGraphEditorWindow> _windows = new Dictionary<NodeGraph, NodeGraphEditorWindow> ();
		static Dictionary<NodeGraph, NodeGraphEditorWindow> Windows
		{
			get
			{
				if (_windows == null)
				{
					_windows = new Dictionary<NodeGraph, NodeGraphEditorWindow> ();
				}
				return _windows;
			}
		}

		[UnityEditor.Callbacks.OnOpenAsset (0)]
		public static bool OnOpenNodeGraph (int instanceID, int line)
		{
			NodeGraph graph = EditorUtility.InstanceIDToObject (instanceID) as NodeGraph;
			if (graph != null)
			{
				var window = GetWindow (graph);
				window.titleContent = new GUIContent (window.NodeGraph.name);
				window.Show ();
				return true;
			}
			return false;
		}

		public static NodeGraphEditorWindow GetWindow (NodeGraph graph)
		{
			if (!Windows.ContainsKey (graph) || Windows[graph] == null)
			{
				var window = CreateInstance<NodeGraphEditorWindow> ();
				window.NodeGraph = graph;
				Windows[graph] = window;
			}
			return Windows[graph];
		}

		private void OnEnable ()
		{
			if (NodeGraph == null)
			{
				return;
			}
			if (!Windows.ContainsKey (NodeGraph))
			{
				Windows[NodeGraph] = this;
			}
		}

		private void OnDisable ()
		{
		}
		#endregion
		#region GUI
		private void OnGUI ()
		{
			//record mouse position
			MousePosition = Event.current.mousePosition;
			Rect rect = position.MoveTo (0, 0);

			//info box
			if (NodeGraph == null)
			{
				Rect rect_info = rect.Resize (Mathf.Max (300, rect.width / 2), 60).MoveCenterTo (rect.center);
				EditorGUI.HelpBox (rect_info, "No available NodeGraph", MessageType.Info);
				return;
			}

			//get graph editor
			NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor (NodeGraph.GetType ());

			//canvas 
			Rect rect_canvas = graphEditor.GetCanvasRect (rect, NodeGraph);//local to editor window
			using (var scope = GUIPanScope.Begin (rect_canvas, NodeGraph.scale))
			{
				MousePosition_Canvas = Event.current.mousePosition;

				//update connecting, drag canvas, zoom, etc
				UpdateCanvasEvents (rect_canvas);

				//draw canvas background
				graphEditor.DrawCanvas (scope.ClipRect, NodeGraph);

				//draw connecting 
				DrawConnectingLine ();

				//draw connections
				DrawConnections ();

				//nodes
				DrawNodes (NodeGraph);
			}

			//canvas context menu
			if (GUI.Button (rect_canvas, "", GUIStyle.none))
			{
				switch (Event.current.button)
				{
					case 1:
						graphEditor.OpenContextMenu (NodeGraph);
						break;
				}
			}

			//overlay
			graphEditor.DrawOverlayGUI (rect, NodeGraph);
		}

		void DrawNodes (NodeGraph graph)
		{
			BeginWindows ();
			var nodes = graph.nodes;
			for (int i = 0; i < nodes.Count; i++)
			{
				var node = nodes[i];
				var nodeEditor = NodeEditor.GetEditor (node.GetType ());
				Rect rect_node = new Rect (node.CanvasPos, nodeEditor.GetSize (node));
				node.position = GUI.Window (i, rect_node, DrawNode, "", GUIStyle.none).position - NodeGraph.position;
			}
			EndWindows ();
		}

		void DrawNode (int nodeIndex)
		{
			var node = NodeGraph.nodes[nodeIndex];
			var nodeEditor = NodeEditor.GetEditor (node.GetType ());
			Rect rect = new Rect (default, nodeEditor.GetSize (node));
			nodeEditor.DrawNode (rect, node, false);
		}

		void DrawConnections ()
		{
			foreach (var node in NodeGraph.nodes)
			{
				foreach (var port in node.nodePorts)
				{
					var nodeEditor = NodeEditor.GetEditor (node.GetType ());
					foreach (var output in port.outputs)
					{
						nodeEditor.DrawConnection (port, output);
					}
				}
			}
		}

		void DrawConnectingLine ()
		{
			if (IsConnecting)
			{
				Vector2 start = sourcePort.CanvasCenter;
				Vector2 end = MousePosition_Canvas;
				Vector2 startTangent = sourcePort.CanvasTangentPoint;
				Vector2 endTangent = end;

				Color color = Color.white;
				float width = 5;

				EditorGUIEX.DrawBezierCurve (start, end, startTangent, endTangent, color, width, true);
				Repaint ();
			}
		}
		#endregion
		#region operations
		void UpdateCanvasEvents (Rect rect_canvas)
		{
			UpdateZoom (rect_canvas);
			UpdateDragCanvas ();
			UpdateConnect ();
		}
		#region zoom
		void UpdateZoom (Rect rect_canvas)
		{
			if (rect_canvas.Contains (MousePosition))
			{
				Event e = Event.current;
				switch (e.type)
				{
					case EventType.ScrollWheel:
						Vector2 normMousePos = rect_canvas.GetNormalizedPosition (MousePosition);
						Vector2 pan_size = rect_canvas.size / NodeGraph.scale;

						NodeGraph.scale -= Mathf.Sign (e.delta.y) * 0.1f;
						NodeGraph.scale = Mathf.Clamp (NodeGraph.scale, NodeGraph.MinScale, NodeGraph.MaxScale);

						Vector2 pan_size_new = rect_canvas.size / NodeGraph.scale;
						Vector2 d_pan_size = pan_size_new - pan_size;

						NodeGraph.position += new Vector2 (d_pan_size.x * normMousePos.x, d_pan_size.y * normMousePos.y);

						e.Use ();
						Repaint ();
						break;
				}
			}
		}
		#endregion
		#region drag canvas
		int controlId_dragCanvas = 0;
		Vector2 lastMousePos;

		public bool IsDraggingCanvas => controlId_dragCanvas != 0 && GUIUtility.hotControl == controlId_dragCanvas;

		void UpdateDragCanvas ()
		{
			controlId_dragCanvas = GUIUtility.GetControlID (FocusType.Passive);
			Event e = Event.current;

			switch (e.GetTypeForControl (controlId_dragCanvas))
			{
				case EventType.MouseDown:
					if (e.button == 2)
					{
						GUIUtility.hotControl = controlId_dragCanvas;
						lastMousePos = MousePosition_Canvas;
						e.Use ();
					}
					break;
				case EventType.MouseUp:
					if (IsDraggingCanvas && e.button == 2)
					{
						GUIUtility.hotControl = 0;
						e.Use ();
					}
					break;
				case EventType.MouseDrag:
					if (IsDraggingCanvas)
					{
						var delta = MousePosition_Canvas - lastMousePos;
						lastMousePos = MousePosition_Canvas;
						NodeGraph.position += delta;
						e.Use ();
						Repaint ();
					}
					break;
			}
		}
		#endregion
		#region connection
		int controlId_connecting = 0;
		NodePort sourcePort;

		public bool IsConnecting => controlId_connecting != 0 && controlId_connecting == GUIUtility.hotControl;

		void UpdateConnect ()
		{
			controlId_connecting = GUIUtility.GetControlID (FocusType.Passive);
			Event e = Event.current;

			if (IsConnecting)
			{
				switch (e.GetTypeForControl (controlId_connecting))
				{
					case EventType.KeyDown:
						if (Event.current.keyCode == KeyCode.Escape)
						{
							if (StopConnect ())
							{
								e.Use ();
							}
						}
						break;
					case EventType.MouseDown:
						StopConnect (FindPortThatContains (MousePosition_Canvas));
						break;
				}
			}

		}

		NodePort FindPortThatContains (Vector2 position)
		{
			foreach (var node in NodeGraph.nodes)
			{
				foreach (var port in node.nodePorts)
				{
					if (port.CanvasRect.Contains (position))
					{
						return port;
					}
				}
			}
			return null;
		}

		public void StartConnect (NodePort sourcePort)
		{
			if (sourcePort.ReachOutputLimit)
			{
				if (sourcePort.outputLimit == 0)
				{
					return;
				}

				switch (sourcePort.OnMaxConnectionBehaviour)
				{
					case Node.PortOnMaxConnectionBehaviours.OverrideFirstConnection:
					case Node.PortOnMaxConnectionBehaviours.OverrideLastConnection:
						break;
					case Node.PortOnMaxConnectionBehaviours.IgnoreNewConnection:
					default:
						return;
				}
			}

			this.sourcePort = sourcePort;
			GUIUtility.hotControl = controlId_connecting;
		}

		public bool StopConnect (NodePort targetPort = null)
		{
			if (IsConnecting)
			{
				GUIUtility.hotControl = 0;
				if (targetPort != null)
				{
					if (sourcePort.ReachOutputLimit)
					{
						switch (sourcePort.OnMaxConnectionBehaviour)
						{
							case Node.PortOnMaxConnectionBehaviours.OverrideFirstConnection:
								sourcePort.RemoveOutput (0);
								break;
							case Node.PortOnMaxConnectionBehaviours.OverrideLastConnection:
								sourcePort.RemoveOutput (sourcePort.OutputCount - 1);
								break;
						}
					}
					if (targetPort.ReachInputLimit)
					{
						switch (targetPort.OnMaxConnectionBehaviour)
						{
							case Node.PortOnMaxConnectionBehaviours.OverrideFirstConnection:
								targetPort.RemoveInput (0);
								break;
							case Node.PortOnMaxConnectionBehaviours.OverrideLastConnection:
								targetPort.RemoveInput (targetPort.InputCount - 1);
								break;
						}
					}
					sourcePort.ConnectTo (targetPort);
				}
				sourcePort = null;
				return true;
			}
			return false;
		}
		#endregion
		#endregion
	}
}